package clasificador.vecinosproximos;

import java.util.*;

import metrica.DistanciaEuclidea;
import metrica.DistanciaVectorial;
import clasificador.ClasificadorEntrenado;
import datos.Atributo;
import datos.Dato;
import utils.IBuilder;
import datos.MuestraClasificada;
import datos.VectorizadorAtributos;

/**
 *
 * @author e235166
 */
public class ClasificadorVecinosProximos<Clase> extends ClasificadorEntrenado<Clase> {
    /* La distancia es la distancia basica a utilizar. El clasificador se encarga de renormalizarla por dentro. */

    private DistanciaVectorial distanciaOriginal;
    private DistanciaNormalizada distanciaNormalizada;
    private VectorizadorAtributos vectorizador;

    /* Numero de vecinos a considerar */
    private Integer k;

    /* Datos originales ya convertidos en vectores y renormalizados */
    private Map<Double[], Clase> elementos;

    public ClasificadorVecinosProximos(MuestraClasificada<Clase> datosEntrenamiento, Map<String, Object> parametros) {
        super(datosEntrenamiento, parametros);
    }

    @Override
    protected void inicializarParametros(Map<String, Object> parametros) {
        if (parametros.get("distancia") == null) {
            parametros.put("distancia", new DistanciaEuclidea());
        }
        if (parametros.get("k") == null) {
            throw new RuntimeException("Debe especificar el numero de vecinos a considerar en la clasificacion.");
        }

        this.k = (Integer) parametros.get("k");
        this.vectorizador = (VectorizadorAtributos) parametros.get("vectorizador");
        this.distanciaOriginal = (DistanciaVectorial) parametros.get("distancia");
    }

    /* Define una distancia entre datos como una distancia vectorial entre sus atributos numericos */
    /* */
    /* Solo renormaliza el segundo elemento comparado. Esto es asi por eficiencia, dado que es una clase privada, de forma que */
    /* siempre se pase como primer elemento el elemento previamente calculado (el que forma parte de los datos), que ya esta normalizado. */
    /* Asi se evita renormalizar los datos originales en cada ocasion */
    private static class DistanciaNormalizada extends DistanciaVectorial {

        private DistanciaVectorial distanciaBase;
        private Double[] factoresNormalizacion;

        private DistanciaNormalizada(DistanciaVectorial distanciaBase, Double[] factoresNormalizacion) {
            this.distanciaBase = distanciaBase;
            this.factoresNormalizacion = factoresNormalizacion;
        }

        @Override
        public double distanciaVectorial(Double[] a, Double[] b) {
            /* Esto seria lo razonable, por eficiencia se hace lo segundo */
            // return distanciaBase.distancia(factorizar(a), factorizar(b));
            return distanciaBase.distancia(a, b);
        }

        private Double[] factorizar(Double[] a) {
            Double[] resultado = new Double[a.length];

            for (int i = 0; i < a.length; i++) {
                resultado[i] = a[i] * factoresNormalizacion[i];
            }

            return resultado;
        }

        private static class Builder implements IBuilder<Double[], DistanciaNormalizada> {

            private Double[] media;
            private Double[] sumaCuadrados;
            private Double cuenta;
            private int dimension;
            private DistanciaVectorial distanciaBase;

            private Builder(int dimension, DistanciaVectorial distanciaBase) {
                this.dimension = dimension;
                this.distanciaBase = distanciaBase;
                this.media = new Double[dimension];
                this.sumaCuadrados = new Double[dimension];
            }

            @Override
            public void add(Double[] c) {
                cuenta++;
                for (int i = 0; i < dimension; i++) {
                    Double delta = c[i] - media[i];
                    media[i] = media[i] + delta / cuenta;
                    sumaCuadrados[i] = sumaCuadrados[i] + delta * (c[i] - media[i]);
                }
            }

            @Override
            public DistanciaNormalizada finish() {
                Double[] factores = new Double[dimension];
                for (int i = 0; i < dimension; i++) {
                    /* El factor de normalizacion es el inverso de la desviacion tipica */
                    factores[i] = Math.sqrt((cuenta - 1) / sumaCuadrados[i]);
                }

                return new DistanciaNormalizada(distanciaBase, factores);
            }

            @Override
            public void reset() {
                cuenta = 0.0;
                for (int i = 0; i < dimension; i++) {
                    media[i] = 0.0;
                    sumaCuadrados[i] = 0.0;
                }
            }
        }
    }

    @Override
    protected void entrenar(MuestraClasificada<Clase> datosEntrenamiento) {
        System.out.flush();

        if (this.vectorizador == null) {
            List<Atributo<?>> atributosOrigen = datosEntrenamiento.getAtributos();
            Atributo<Double>[] atributos = new Atributo[datosEntrenamiento.getAtributos().size() - 1];

            for (int i = 0, j = 0; i < datosEntrenamiento.getAtributos().size(); i++) {
                Atributo<?> atributo = atributosOrigen.get(i);
                if (!atributo.equals(datosEntrenamiento.getClase())) {
                    if (!Number.class.isAssignableFrom(atributo.getTipo())) {
                        throw new RuntimeException("Non-numerical attribute in KNN classifier");
                    }
                    atributos[j] = (Atributo<Double>) atributo;
                    j++;
                }
            }

            this.vectorizador = new VectorizadorAtributos(atributos);
        }

        /* Hacemos dos pasadas a los datos a cambio de evitar calculos extra durante la clasificacion. Es un compromiso que hemos considerado adecuado, pero depende del uso */
        /* No se puede hacer todo de una pasada porque para guardar los datos renormalizados hay que haber calculado las distribuciones previamente */

        /* Guardar los datos, pero ya listos para hacer distancias (en formato de array de doubles y renormalizados) */
        this.elementos = new HashMap<Double[], Clase>(datosEntrenamiento.getDatos().size());

        /* Calcular la renormalizacion de los atributos (construir distancia renormalizada) */
        DistanciaNormalizada.Builder builder = new DistanciaNormalizada.Builder(datosEntrenamiento.getAtributos().size() - 1, this.distanciaOriginal);
        builder.reset();
        for (Dato dato : datosEntrenamiento.getDatos()) {        	
            Double[] dd = vectorizador.adapt(dato);
            builder.add(vectorizador.adapt(dato));
            //this.elementos.put(dd, dato.getAtributo(datosEntrenamiento.getClase()));
        }
        this.distanciaNormalizada = builder.finish();


        for(Dato dato: datosEntrenamiento.getDatos()) {
            this.elementos.put(distanciaNormalizada.factorizar(vectorizador.adapt(dato)),dato.getAtributo(datosEntrenamiento.getClase()));
        }
        System.out.flush();
    }

    @Override
    public Clase clasificar(Dato d) {
        Double[] dd = this.distanciaNormalizada.factorizar(vectorizador.adapt(d));
        //Comparator<Double[]> comparator = new ComparadorClasificacion(vectorizador.adapt(d));

        // Esto es demasiado lento
        //SortedMap<Double[],Clase> vecinosProximos = new TreeMap<Double[],Clase>(comparator);

        ArrayList<Double> distancias = new ArrayList<>();
        ArrayList<Clase> clases = new ArrayList<>();

        for (Map.Entry<Double[], Clase> vecino : this.elementos.entrySet()) {
            double distancia = this.distanciaNormalizada.distancia(vecino.getKey(), dd);

            if (distancias.size() < this.k) {
                distancias.add(distancia);
                clases.add(vecino.getValue());
            } else {
                double max = distancia;
                int indexMax = -1;
                for (int i = 0; i < distancias.size(); i++) {
                    if (distancias.get(i) > max) {
                        max = distancias.get(i);
                        indexMax = i;
                    }
                }
                if (indexMax != -1) {
                    Clase claseVieja = clases.get(indexMax);
                    Clase claseNueva = vecino.getValue();
                    distancias.set(indexMax, distancia);
                    clases.set(indexMax, claseNueva);
                }
            }
            /* Primero metemos el nuevo elemento y luego, si nos sobran, quitamos el �ltimo. */
            /* Esto es m�s eficiente que quedarnos con los k primeros al final, pues evitamos tener que ordenar entre elementos */
            /* que, en cualquier caso, est�n muy lejos de nuestro dato */
            /* vecinosProximos.put(vecino.getKey(), vecino.getValue());
             if(vecinosProximos.size() > this.k)
             {
             vecinosProximos.remove(vecinosProximos.lastKey());
             } */
        }

        Map<Clase, Integer> frecuencias = new HashMap();
        Map<Clase, Double> sumaDistancias = new HashMap();
        for (int i = 0; i < clases.size(); i++) {
            Clase c = clases.get(i);
            Double dist = distancias.get(i);

            Integer frecuencia = frecuencias.get(c);
            Double suma = sumaDistancias.get(c);
            if (frecuencia == null) {
                frecuencia = 1;
            } else {
                frecuencia++;
            }
            if (suma == null) {
                suma = dist;
            } else {
                suma += dist;
            }

            frecuencias.put(c, frecuencia);
            sumaDistancias.put(c, suma);
        }

        Set<Clase> clasesSet = new HashSet<>(clases);
        Clase mejorClase = null;
        Integer mejorFrecuencia = 0;
        Double mejorDistancia = 0.0;
        for (Clase c : clasesSet) {
            if (frecuencias.get(c) > mejorFrecuencia) {
                mejorFrecuencia = frecuencias.get(c);
                mejorClase = c;
                mejorDistancia = sumaDistancias.get(c);
            } else if (frecuencias.get(c) == mejorFrecuencia) {
                if (mejorDistancia > sumaDistancias.get(c)) {
                    mejorFrecuencia = frecuencias.get(c);
                    mejorClase = c;
                    mejorDistancia = sumaDistancias.get(c);
                }
            }
        }
        /* Cogemos la clase mas frecuente entre los vecinos proximos */
        return mejorClase;
    }
}
